<template>
  <div class="chat-btn" @click="openChat">
    <img src="@/assets/img/ai.png"/>
  </div>
  <div class="chat-container" :class="showChat ? 'show-chat' : ''">
    <div class="chat-close" @click="close">
      <i class="iconfont icon-guanbi"></i>
    </div>
    <div class="chat-list">
      <el-scrollbar ref="scroll">
        <transition-group name="message" tag="div" class="message-list">
          <!--          <div class="item left">
                      <div class="shape"></div>
                      您好，客服ai参谋为您服务，有什么可以帮您，你好！
                    </div>-->
          <div
            class="item"
            :class="item.self ? 'self right' : 'left'"
            v-for="(item, idx) in list"
            :key="idx"
          >
            <div class="shape"></div>
            <div v-if="typeof item.content === 'string'">
              {{ item.content }}
            </div>
            <div v-else>
              <div v-for="subItem in item.content" :key="subItem.type">
                <div v-if="subItem.type === 'text'">{{ subItem.text }}</div>
                <div v-else-if="subItem.type === 'image_url'">
                  <img :src="subItem.image_url.url" alt="Image" style="max-width: 100%"/>
                </div>
              </div>
            </div>
            <div class="msg-status" :class="item.self || 'msg-status-right'">
              <el-icon v-if="item.state === 'loading'"><Loading /></el-icon>
              <el-icon v-else-if="item.state === 'success'"><SuccessFilled /></el-icon>
              <el-icon v-else-if="item.state === 'error'"><CircleCloseFilled /></el-icon>
            </div>
          </div>
        </transition-group>
      </el-scrollbar>
    </div>
    <div class="msg-bar">
      <!--      <div class="photo icon">-->
      <!--        <i class="iconfont icon-xiangji"></i>-->
      <!--      </div>-->
      <div class="input" id="msg-input">
        <el-input
          @keydown.enter="sendMsg"
          v-model="msg"
          autocomplete="off"
          :disabled="loading"
          placeholder="请输入信息"
        />
      </div>
      <!--      <div class="photo icon" style="margin-right: 8px">
              <i class="iconfont icon-yuyin"></i>
            </div>-->
<!--      <div class="photo icon">
        <el-upload
          class="upload-demo"
          :action="''"
          :show-file-list="false"
          :before-upload="beforeUpload"
          :on-change="handleFileChange"
          :disabled="loading"
        >
          <i class="iconfont icon-tupian"></i>
        </el-upload>
      </div>-->
    </div>
  </div>
</template>

<script>
import mqtt from 'mqtt'
import * as api from '@/api'
import SseClient from "@/components/sse-client";
import {CircleCloseFilled, Loading, SuccessFilled} from "@element-plus/icons-vue";

let mqttClient = null;
export default {
  name: 'Chat',
  components: {Loading, CircleCloseFilled, SuccessFilled},
  data() {
    return {
      topic: '',
      loading: false,
      list: [],
      msg: '',
      showChat: false,
      eventSource: null,
      sseClient: null
    };
  },
  methods: {
    openChat() {
      this.showChat = !this.showChat;
      this.registerSse()
    },
    close() {
      this.showChat = false;
    },
    async registerSse() {
      this.sseClient = new SseClient()
      this.sseClient.registerSse(
        (data) => {
          // 解析data为对象
          if (typeof data === 'string') {
            try {
              data = JSON.parse(data);
            } catch (e) {
              return;
            }
          }
          if (data.type === 'message') {
            // 消息内容格式为 {msgId}:{text}
            const [msgId, ...textArr] = (data.data || '').split(':');
            const text = textArr.join(':');
            // 查找是否已存在该msgId的assistant消息
            let idx = this.list.findIndex(item => item.msgId === msgId);
            if (idx === -1) {
              // 没有则新建
              this.list.push({
                self: false,
                msgId,
                content: text,
                state: 'loading'
              });
            } else {
              // 已有则直接追加内容，不判断类型
              this.list[idx].content += text;
            }
            this.scrollToBottom();
          } else if (data.type === 'complete') {
            // 消息内容为 {msgId}
            const msgId = data.data;
            let idx = this.list.findIndex(item => item.msgId === msgId);
            if (idx !== -1) {
              console.log(this);
              this.list[idx].state = 'success';
            }
            this.loading = false;
          } else if (data.type === 'error') {
            this.$message.error('发生错误: ' + data.data);
            this.loading = false;
          }
        },
        (error) => {
          console.error('SSE Error:', error);
          this.loading = false;
        },
      );
    },
    
    async startSSE() {
      const token = localStorage.getItem('token');
      let msgList = [];
      this.list.forEach((item) => {
        // 检查消息类型，图片消息转为适当的格式
        if (Array.isArray(item.content)) {
          msgList.push({
            role: item.self ? 'user' : 'assistant',
            content: item.content.map((subItem) => ({
              type: subItem.type,
              text: subItem.text || undefined,
              image_url: subItem.image_url || undefined,
            })),
          });
        } else {
          msgList.push({
            role: item.self ? 'user' : 'assistant',
            content: item.content,
          });
        }
      });
      
      // 根据消息类型动态设置模型
      const model = this.isImageMessage(msgList)
        ? 'qwen-vl-plus'
        : 'qwen2.5-14b-instruct';
      
      const params = {
        stream: true,
        messages: msgList,
        model,
      };
      try {
        // 发送请求
        let response = await fetch(
          'http://visionsd.yunwo.space/api/gpt/v1/chat',
          {
            method: 'post',
            responseType: 'stream',
            headers: {
              'Content-Type': 'application/json',
              Authorization: `Bearer ${token}`,
            },
            body: JSON.stringify(params),
          },
        );
        // ok字段判断是否成功获取到数据流
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        this.loading = true;
        // 用来获取一个可读的流的读取器（Reader）以流的方式处理响应体数据
        const reader = response.body.getReader();
        // 将流中的字节数据解码为文本字符串
        const textDecoder = new TextDecoder();
        let result = true;
        this.list.push({
          self: false,
          content: '',
        });
        let idx = this.list.length - 1;
        while (result) {
          // done表示流是否已经完成读取  value包含读取到的数据块
          const {done, value} = await reader.read();
          if (done) {
            result = false;
            break;
          }
          // 拿到的value就是后端分段返回的数据，大多是以data:开头的字符串
          // 需要通过decode方法处理数据块，例如转换为文本或进行其他操作
          const chunkText = textDecoder
            .decode(value)
            .split('\n')
            .forEach((val) => {
              if (!val) return;
              try {
                // 后端返回的流式数据一般都是以data:开头的字符，排除掉data:后就是需要的数据
                // 具体返回结构可以跟后端约定
                let text = val?.replace('data:', '') || '';
                let textJson = JSON.parse(text);
                this.list[idx].content += textJson.choices[0].delta.content;
                if (textJson.choices[0].finish_reason == 'stop') {
                  this.loading = false;
                }
                this.scrollToBottom();
              } catch (err) {
              }
            });
        }
      } catch (err) {
        console.log(err, 66);
      } finally {
      }
    },
    scrollToBottom() {
      this.$nextTick(() => {
        let wrap = this.$refs['scroll'].wrapRef;
        let view = wrap.getElementsByClassName('el-scrollbar__view')[0];
        this.$refs['scroll'].setScrollTop(view.offsetHeight);
      });
    },
    async sendMsg() {
      if (this.msg === '') {
        return;
      }
      const msgId = Date.now().toString();
      const content = this.msg.trim();
      this.list.push({
        self: true,
        msgId: 'send' + msgId,
        content: content,
        state: 'loading',
      });
      this.msg = '';
      this.loading = true;
      const {status, data} = await this.sseClient.sendMessage({
        text: content,
        sessionId: this.sessionId,
        msgId,
      });
      const idx = this.list.findIndex(item => item.msgId === 'send' + msgId);
      if (status === 200 && data.code === 0) {
        // 根据 'send' + msgId 来找到对应的消息
        if (idx !== -1) {
          this.list[idx].state = 'success';
        }
      } else {
        if (idx !== -1) {
          this.list[idx].state = 'error';
        }
        this.$message.error('消息发送失败，请稍后再试');
      }
     
      this.scrollToBottom();
    },
    beforeUpload(file) {
      // return false;
      // const isImage = file.type.startsWith('image/');
      // if (!isImage) {
      //   this.$message.error('只能上传图片文件');
      // }
      // return isImage;
    },
    handleFileChange(file, fileList) {
      // 上传成功，获取返回的文件路径并将其转换为base64
      const reader = new FileReader();
      reader.onloadend = () => {
        const base64Data = reader.result.split(',')[1]; // 提取Base64部分
        this.addImageMessage(base64Data);
      };
      reader.readAsDataURL(file.raw);
    },
    addImageMessage(base64Data) {
      const textMessages = [];
      
      // 只有在消息中存在文字时才将其添加到数组
      if (this.msg) {
        textMessages.push({
          type: 'text',
          text: this.msg,
        });
      }
      
      // 将图片消息添加到数组
      textMessages.push({
        type: 'image_url',
        image_url: {url: `data:image/jpeg;base64,${base64Data}`},
      });
      
      // 如果只有图片消息，则只发送图片
      if (textMessages.length === 1 && textMessages[0].type === 'image_url') {
        this.list.push({
          self: true,
          content: [
            {
              type: 'image_url',
              image_url: {url: `data:image/jpeg;base64,${base64Data}`},
            },
          ],
        });
      } else {
        this.list.push({
          self: true,
          content: textMessages, // 传图文
        });
      }
      // 清空 msg 内容
      this.msg = '';
      this.startSSE();
      this.scrollToBottom();
    },
    // 判断是否是图片消息
    isImageMessage(msgList) {
      return msgList.some(
        (item) =>
          Array.isArray(item.content) &&
          item.content.some((subItem) => subItem.type === 'image_url'),
      );
    },
    async init() {
      let that = this;
      try {
        const {data} = await api.chatRegister()
        if (!data.code) {
          let info = data.data
          this.topic = info.topic
          const options = {
            clean: true,
            clientId: 'yswn-web-' + Math.random().toString(16).substr(2, 8),
            username: info.username,
            password: info.password,
          }
          mqttClient = mqtt.connect(info.url, options)
          mqttClient.on('connect', function () {
            mqttClient.subscribe(info.topic, function (err) {
              if (!err) {
                console.log('Subscribed Success')
              }
            })
          })
          
          // 接收消息
          mqttClient.on('message', function (tp, message) {
            const msg = message.toString()
            
            if (msg.startsWith('send::')) {
              return
            } else {
              that.list.push({
                self: false,
                content: msg,
              });
            }
            
          })
        }
      } catch (e) {
      
      }
    }
  },
  mounted() {
    // this.init()
  }
};
</script>

<style scoped lang="scss">
.message-enter-from {
  opacity: 0;
  transform: translateY(20px);
}

.message-enter-active {
  transition: all 0.3s ease;
  transition-delay: 0.1s;
}

.message-leave-to {
  opacity: 0;
  transform: translateY(-20px);
}

.message-leave-active {
  transition: all 0.3s ease;
  transition-delay: 0.1s;
}

.chat-btn {
  position: fixed;
  bottom: 25px;
  width: 120px;
  height: 120px;
  right: 25px;
  z-index: 555;
  cursor: pointer;
  border-radius: 50%;
  
  img {
    @include fit(contain);
    box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.25);
  }
}

.chat-container {
  width: 600px;
  height: 871px;
  background: #2d2d2d;
  box-shadow: -20px -20px 30px 0px rgba(0, 0, 0, 0.59);
  border-radius: 32px 32px 32px 32px;
  position: fixed;
  bottom: 50px;
  left: 50px;
  overflow: hidden;
  z-index: 555;
  opacity: 0;
  transition: 0.3s ease;
  transform: translateY(0px);
  visibility: hidden;
  
  &.show-chat {
    opacity: 1;
    transform: translateY(-10px);
    visibility: visible;
  }
  
  .chat-close {
    width: 24px;
    height: 24px;
    position: absolute;
    right: 20px;
    top: 20px;
    cursor: pointer;
    color: rgba(221, 221, 221, 1);
    display: flex;
    align-items: center;
    justify-content: center;
    
    &:hover {
      i {
        color: #fff;
      }
    }
    
    i {
      transition: 0.3s ease;
      font-size: 24px;
    }
  }
  
  .chat-list {
    width: 100%;
    height: calc(100% - 64px);
    margin-top: 64px;
    box-sizing: border-box;
    padding-bottom: 100px;
    
    .item {
      width: calc(100% - 90px);
      margin-left: 45px;
      background: #3f3f3f;
      margin-bottom: 40px;
      border-radius: 18px;
      padding: 16px;
      box-sizing: border-box;
      line-height: 24px;
      font-weight: 500;
      font-size: 16px;
      color: #ffffff;
      position: relative;
      word-break: break-all;
      
      .shape {
        width: 16px;
        height: 60px;
        background: #3f3f3f;
        position: absolute;
        left: -16px;
        top: 0px;
        overflow: hidden;
        
        &:before {
          content: '';
          width: 500px;
          height: 500px;
          background: #2d2d2d;
          position: absolute;
          right: 0px;
          top: -50px;
          border-radius: 96px;
        }
      }
      
      .msg-status{
        position: absolute;
        left: 10px;
        bottom: -22px;
        font-size: 16px;
        color: #ffffff;
        height: 20px;
        width: 20px;
      }
      .msg-status-right{
        right: 10px;
        left: unset;
      }
    }
    
    .left {
      border-top-left-radius: 0px;
    }
    
    .right {
      border-top-right-radius: 0px;
    }
    
    .self {
      background: $color;
      
      .shape {
        left: unset;
        right: -16px;
        background: $color;
        
        &:before {
          color: $color;
          left: 0px;
          right: unset;
        }
      }
    }
  }
  
  .msg-bar {
    position: absolute;
    bottom: 30px;
    left: 30px;
    width: 540px;
    height: 70px;
    background: #555555;
    box-shadow: 10px 10px 10px 0px rgba(0, 0, 0, 0.2);
    border-radius: 16px 16px 16px 16px;
    @include clear;
    box-sizing: border-box;
    padding: 0 20px;
    display: flex;
    align-items: center;
    
    .icon {
      width: 32px;
      height: 32px;
      float: left;
      display: flex;
      align-items: center;
      justify-content: center;
      
      i {
        font-size: 20px;
        color: #fff;
      }
    }
    
    .input {
      float: left;
      /*width: calc(100% - 32px * 3 - 8px);*/
      width: calc(100%);
    }
    
    #msg-input {
      :deep(.el-input) {
        .el-input__wrapper {
          background: none;
        }
        
        .el-input__inner {
          font-size: 16px;
        }
      }
    }
  }
}
</style>

